Trabalho de Estatística Descritiva - Aplicação no Mercado Financeiro¶

Universidade de Brasília - PPCA

Aluno: Paulo Célio Soares da Silva Júnior - Matrícula: 220005605

Curso: AEDI 1/2022

Prof. João Gabriel de Moraes Souza


1 - Importando as bibliotecas


In [1]:
import pandas as pd
import pandas_datareader.data as web
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
from scipy import optimize

2 - Conhecendo o dataset


No primeiro dia de cada mês, são recomendados 10 ativos para investir, selecionados por analistas da corretora NuInvest, que compõem uma carteira denominada Top 10 recomendações. A recomendação dos analistas é que o investimento seja distribuído igualmente, ou seja, em cada um desses ativos, aplica-se 10% do total investido.

Para o mês de julho de 2022, a corretora indicou os seguintes ativos para composição da carteira:


Petrorio

PRIO3

JHSF Part

JHSF3

Santander

SANB11

Telefônica Brasil

VIVT3

Eneva

ENEV3

Multiplan

MULT3

Simpar

SIMH3

Unipar

UNIP6

Santos Brp

STBP3

Gerdau Met

GOAU4

A partir dessa carteira, o dataset de estudo foi construído a fim de obter, como amostras, os valores de mercado dessas ações desde 2015 até os dias atuais. A título de comparação, o dataset também incorpora o índice IBOVESPA (^BVSP).

2.1 - Conhecendo a API para obtenção dos dados de ações¶


Para conhecer a API, são obtidos dados de uma ação negociada na B3, no caso da Telefônica (VIVT3). A fonte dos dados a ser utilizada nesta análise é o Yahoo Finance.Em seguida, são obtidos os 5 primeiros registros para teste e exibição da estrutura do DataFrame Pandas gerado pelo serviço. O serviço retorna em sua estrutura as seguintes colunas:

  • High: maior preço do ativo no dia de negociação;
  • Low: menor preço no dia;
  • Open: preço de abertura;
  • Close: preço de fechamento;
  • Volume: volume total de ações negociadas no dia;
  • Adj Close: preço de fechamento ajustado.
In [2]:
telefonica_df = web.DataReader(name="VIVT3.SA",
                           data_source="yahoo", start="2000-01-01")
telefonica_df.head()
Out[2]:
High Low Open Close Volume Adj Close
Date
2000-01-03 24.990000 23.700001 24.990000 24.000000 98.0 7.796522
2000-01-04 23.700001 22.750000 23.100000 23.350000 228.0 7.585369
2000-01-05 23.700001 22.000000 23.700001 23.610001 120.0 7.669831
2000-01-06 24.129999 23.100000 23.400000 24.129999 123.0 7.838752
2000-01-07 24.250000 23.600000 24.000000 24.250000 266.0 7.877736

2.2 - Construindo o dataset com as ações da carteira¶


A seguir, os códigos dos ativos são reunidos em uma lista para montagem do DataFrame do Pandas com preço de fechamento de cada um dos ativos que compõem a carteira. A data de início é 01/01/2015 e a de término é a data corrente.

In [3]:
acoes = ["PRIO3.SA", "JHSF3.SA", "SANB11.SA", "VIVT3.SA", "ENEV3.SA", "MULT3.SA", "SIMH3.SA", "UNIP6.SA", "STBP3.SA", "GOAU4.SA", "^BVSP"]
acoes
Out[3]:
['PRIO3.SA',
 'JHSF3.SA',
 'SANB11.SA',
 'VIVT3.SA',
 'ENEV3.SA',
 'MULT3.SA',
 'SIMH3.SA',
 'UNIP6.SA',
 'STBP3.SA',
 'GOAU4.SA',
 '^BVSP']
In [4]:
acoes_df = pd.DataFrame()
for acao in acoes:
    acoes_df[acao[:5]] = web.DataReader(acao, data_source='yahoo', start='2015-01-01')['Close']

acoes_df.reset_index(inplace=True) # transforma o índice original (data de negociação) em um índice numérico
acoes_df.rename(columns={'Date': 'Data de Negociação'}, inplace=True) # renomeia a coluna "Date" para "Data de Negociação"
acoes_df
Out[4]:
Data de Negociação PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
0 2015-01-02 0.440000 2.23 12.720000 37.820000 5.989072 14.753333 NaN 3.357142 0.95 10.82 48512.0
1 2015-01-05 0.407000 2.15 12.630000 37.070000 5.561281 14.733333 NaN 3.214285 0.95 10.31 47517.0
2 2015-01-06 0.367000 2.16 12.720000 36.150002 5.703878 15.466666 NaN 3.285713 0.95 11.32 48001.0
3 2015-01-07 0.366000 2.16 13.250000 37.389999 5.846475 16.000000 NaN 3.285713 0.95 12.15 49463.0
4 2015-01-08 0.378000 2.36 13.030000 38.910000 5.989072 15.830000 NaN 3.285713 0.95 11.74 49943.0
... ... ... ... ... ... ... ... ... ... ... ... ...
1875 2022-07-20 22.260000 5.81 28.240000 46.889999 14.750000 23.900000 9.51 88.879997 6.27 9.95 98287.0
1876 2022-07-21 22.840000 5.68 28.299999 46.980000 14.530000 23.740000 9.54 88.470001 6.22 9.92 99033.0
1877 2022-07-22 22.670000 5.60 27.530001 46.830002 14.350000 23.980000 9.55 88.500000 6.25 9.94 98925.0
1878 2022-07-25 23.610001 5.59 28.020000 46.419998 14.490000 23.799999 9.50 88.790001 6.23 10.01 100270.0
1879 2022-07-26 23.740000 5.36 27.969999 46.389999 14.410000 23.540001 9.21 86.400002 6.15 10.01 99772.0

1880 rows × 12 columns

2.3 - Visualizando o histórico de preços das ações da carteira¶


O gráfico de linha abaixo ilustra o histórico de preço das ações desde 2015. Para fins de comparação de desempenho apenas dos títulos da carteira, não se considerou o IBOVESPA para renderização do gráfico.

In [5]:
figura = px.line(title = "Histórico do preço das ações")
for i in acoes_df.columns[1:acoes_df.shape[1] - 1]:
  figura.add_scatter(x=acoes_df['Data de Negociação'] ,y=acoes_df[i], name=i)
figura.show()

Sem nenhuma verificão adicional, só pela observação do gráfico, percebe-se, em praticamente todos os ativos, no início de 2020, quedas no valor de fechamento, muito provavelmente, em decorrência do início da pandemia de COVID-19.


3 - Analisando os dados


3.1 - Taxa de Retorno de Ações¶


Para análise da taxa de retorno diário das ações, utilizou-se cálculo de log-retorno em vez de retorno linear, embora, na prática, a diferença seja pouca para dias consecutivos. O log-retorno é dado pela seguinte fórmula:

$$ \mathbb{E} [ R_i] = ln \left( \frac{P_t}{P_{t-1}} \right) $$

Onde:

$ln$ = logaritmo natural

$P_t$ = preço no dia final

$P_{t-1}$ = preço no dia inicial

3.1.1 - Preparando o novo dataset¶

Inicialmente, para que fosse preservada a base de dados original, criou-se uma cópia para que as manipulações pudessem ser realizadas adequadamente. Além disso, removeu-se a coluna "Data de Negociação" para facilitar cálculos de medidas de tendência central, separatrizes e de dispersão no dataset derivado, no caso, um DataFrame do Pandas, uma vez que não faz sentido calcular retorno sobre informação de datas.

In [6]:
dataset = acoes_df.copy()
dataset.drop(labels = ["Data de Negociação"], axis=1, inplace=True)
dataset
Out[6]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
0 0.440000 2.23 12.720000 37.820000 5.989072 14.753333 NaN 3.357142 0.95 10.82 48512.0
1 0.407000 2.15 12.630000 37.070000 5.561281 14.733333 NaN 3.214285 0.95 10.31 47517.0
2 0.367000 2.16 12.720000 36.150002 5.703878 15.466666 NaN 3.285713 0.95 11.32 48001.0
3 0.366000 2.16 13.250000 37.389999 5.846475 16.000000 NaN 3.285713 0.95 12.15 49463.0
4 0.378000 2.36 13.030000 38.910000 5.989072 15.830000 NaN 3.285713 0.95 11.74 49943.0
... ... ... ... ... ... ... ... ... ... ... ...
1875 22.260000 5.81 28.240000 46.889999 14.750000 23.900000 9.51 88.879997 6.27 9.95 98287.0
1876 22.840000 5.68 28.299999 46.980000 14.530000 23.740000 9.54 88.470001 6.22 9.92 99033.0
1877 22.670000 5.60 27.530001 46.830002 14.350000 23.980000 9.55 88.500000 6.25 9.94 98925.0
1878 23.610001 5.59 28.020000 46.419998 14.490000 23.799999 9.50 88.790001 6.23 10.01 100270.0
1879 23.740000 5.36 27.969999 46.389999 14.410000 23.540001 9.21 86.400002 6.15 10.01 99772.0

1880 rows × 11 columns

3.1.2 - Calculando os log-retornos¶

A partir do dataset preparado, aplicou-se a fómula do log-retorno utilizando-se a função de logaritmo natural da biblioteca NumPy sobre o dataset preparado, dividido por um segundo dataset com os índices originais deslocados em uma posição. Essa técnica foi utilizada para que o cálculo fosse feito utilizando-se o preço do dia corrente e o preço do dia anterior, sem a necessidade de fazer um laço por todo o dataset preparado.

In [7]:
dataset_dia_anterior = dataset.shift(1)
taxas_retorno = np.log(dataset / dataset_dia_anterior)
taxas_retorno
Out[7]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 -0.077962 -0.036534 -0.007101 -0.020030 -0.074108 -0.001357 NaN -0.043485 0.000000 -0.048282 -0.020724
2 -0.103451 0.004640 0.007101 -0.025131 0.025318 0.048575 NaN 0.021979 0.000000 0.093457 0.010134
3 -0.002729 0.000000 0.040822 0.033726 0.024693 0.033902 NaN 0.000000 0.000000 0.070758 0.030003
4 0.032261 0.088553 -0.016743 0.039848 0.024098 -0.010682 NaN 0.000000 0.000000 -0.034327 0.009657
... ... ... ... ... ... ... ... ... ... ... ...
1875 -0.004482 0.012121 -0.007057 -0.013136 0.018475 -0.001254 0.038590 0.007000 0.022582 0.002012 0.000427
1876 0.025722 -0.022629 0.002122 0.001918 -0.015028 -0.006717 0.003150 -0.004624 -0.008006 -0.003020 0.007561
1877 -0.007471 -0.014185 -0.027585 -0.003198 -0.012465 0.010059 0.001048 0.000339 0.004812 0.002014 -0.001091
1878 0.040628 -0.001787 0.017642 -0.008794 0.009709 -0.007535 -0.005249 0.003271 -0.003205 0.007018 0.013505
1879 0.005491 -0.042015 -0.001786 -0.000646 -0.005536 -0.010984 -0.031002 -0.027286 -0.012924 0.000000 -0.004979

1880 rows × 11 columns

3.2 - Realizando análise descritiva sobre as taxas de retorno obtidas¶


Em seguida, a partir do dataset preparado, é feita uma análise descritiva onde são calculadas medidas de tendência central (média), separatrizes (quartis) e de dispersão (desvio padrão), além dos valores máximo, mínimo e quantidade de avaliação das taxas de retorno obtidas. Essas informações são exibidas como um novo DataFrame do Pandas.

In [8]:
taxas_retorno.describe()
Out[8]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
count 1879.000000 1879.000000 1879.000000 1879.000000 1879.000000 1879.000000 457.000000 1879.000000 1879.000000 1879.000000 1863.000000
mean 0.002122 0.000467 0.000419 0.000109 0.000467 0.000249 0.000494 0.001729 0.000994 -0.000041 0.000353
std 0.047506 0.033657 0.023569 0.018978 0.034396 0.023804 0.029212 0.028747 0.037384 0.034234 0.016408
min -0.454770 -0.198070 -0.144726 -0.131920 -0.441833 -0.253473 -0.100117 -0.344616 -0.268459 -0.239184 -0.159930
25% -0.019508 -0.018495 -0.012980 -0.009725 -0.010798 -0.012260 -0.017109 -0.011531 -0.009934 -0.018100 -0.007851
50% 0.000000 0.000000 0.000000 0.000000 0.000000 -0.000370 0.000734 0.000000 0.000000 0.000000 0.000589
75% 0.020931 0.016807 0.013628 0.010176 0.011576 0.012250 0.018658 0.013989 0.010110 0.019277 0.009264
max 0.607989 0.254234 0.125840 0.116196 0.287682 0.159602 0.077118 0.182322 1.066524 0.187627 0.130223

3.2.1 - Calculando as médias das taxas de retorno¶

Aqui são calculadas as médias das taxas de retorno das ações de duas formas (em percentual):

  1. pela fórmula da média; e
  2. função mean() nativa de um DataFrame do Pandas.
In [9]:
medias = (taxas_retorno.sum() / len(taxas_retorno)) * 100
medias
Out[9]:
PRIO3    0.212135
JHSF3    0.046647
SANB1    0.041913
VIVT3    0.010864
ENEV3    0.046701
MULT3    0.024853
SIMH3    0.011999
UNIP6    0.172761
STBP3    0.099348
GOAU4   -0.004139
^BVSP    0.034959
dtype: float64
In [10]:
taxas_retorno.mean() * 100
Out[10]:
PRIO3    0.212248
JHSF3    0.046672
SANB1    0.041935
VIVT3    0.010870
ENEV3    0.046726
MULT3    0.024866
SIMH3    0.049363
UNIP6    0.172852
STBP3    0.099401
GOAU4   -0.004141
^BVSP    0.035278
dtype: float64

3.2.2 Calculando a variância das taxas de retorno¶

Aqui são calculadas as variâncias amostrais das taxas de retorno das ações de duas formas:

  1. pela fórmula da variância; e
  2. função var() (normalizada com N-1) nativa de um DataFrame do Pandas.
In [11]:
vars_acoes = ((taxas_retorno - taxas_retorno.mean()) ** 2).sum() / (len(taxas_retorno) - 1)
vars_acoes
Out[11]:
PRIO3    0.002256
JHSF3    0.001132
SANB1    0.000555
VIVT3    0.000360
ENEV3    0.001182
MULT3    0.000566
SIMH3    0.000207
UNIP6    0.000826
STBP3    0.001397
GOAU4    0.001171
^BVSP    0.000267
dtype: float64
In [12]:
taxas_retorno.var()
Out[12]:
PRIO3    0.002257
JHSF3    0.001133
SANB1    0.000555
VIVT3    0.000360
ENEV3    0.001183
MULT3    0.000567
SIMH3    0.000853
UNIP6    0.000826
STBP3    0.001398
GOAU4    0.001172
^BVSP    0.000269
dtype: float64

3.2.3 - Calculando o desvio padrão das taxas de retorno¶

Por fim, é calculado o desvio padrão das taxas de retorno das ações pela função std() (normalizada com N-1) de um DataFrame do Pandas (em percentual).

In [13]:
taxas_retorno.std() * 100
Out[13]:
PRIO3    4.750557
JHSF3    3.365743
SANB1    2.356878
VIVT3    1.897803
ENEV3    3.439600
MULT3    2.380383
SIMH3    2.921168
UNIP6    2.874708
STBP3    3.738410
GOAU4    3.423359
^BVSP    1.640759
dtype: float64

3.3 - Visualizando o comportamento das taxas de retornos das ações da carteira¶


3.3.1 - Incorporando a variável Data de Negociação ao dataset com as Taxas de Retorno¶

A partir do dataset original, busca-se primeiramente as datas das negociações e depois realiza-se a incorporação no dataset contendo as taxas de retorno calculadas. Para isso utiliza-se a função concat() do DataFrame do Pandas.

In [14]:
dataset_date = acoes_df.copy()
date = dataset_date.filter(["Data de Negociação"])
date
Out[14]:
Data de Negociação
0 2015-01-02
1 2015-01-05
2 2015-01-06
3 2015-01-07
4 2015-01-08
... ...
1875 2022-07-20
1876 2022-07-21
1877 2022-07-22
1878 2022-07-25
1879 2022-07-26

1880 rows × 1 columns

In [15]:
taxas_retorno_date = pd.concat([date, taxas_retorno], axis=1)
taxas_retorno_date
Out[15]:
Data de Negociação PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
0 2015-01-02 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 2015-01-05 -0.077962 -0.036534 -0.007101 -0.020030 -0.074108 -0.001357 NaN -0.043485 0.000000 -0.048282 -0.020724
2 2015-01-06 -0.103451 0.004640 0.007101 -0.025131 0.025318 0.048575 NaN 0.021979 0.000000 0.093457 0.010134
3 2015-01-07 -0.002729 0.000000 0.040822 0.033726 0.024693 0.033902 NaN 0.000000 0.000000 0.070758 0.030003
4 2015-01-08 0.032261 0.088553 -0.016743 0.039848 0.024098 -0.010682 NaN 0.000000 0.000000 -0.034327 0.009657
... ... ... ... ... ... ... ... ... ... ... ... ...
1875 2022-07-20 -0.004482 0.012121 -0.007057 -0.013136 0.018475 -0.001254 0.038590 0.007000 0.022582 0.002012 0.000427
1876 2022-07-21 0.025722 -0.022629 0.002122 0.001918 -0.015028 -0.006717 0.003150 -0.004624 -0.008006 -0.003020 0.007561
1877 2022-07-22 -0.007471 -0.014185 -0.027585 -0.003198 -0.012465 0.010059 0.001048 0.000339 0.004812 0.002014 -0.001091
1878 2022-07-25 0.040628 -0.001787 0.017642 -0.008794 0.009709 -0.007535 -0.005249 0.003271 -0.003205 0.007018 0.013505
1879 2022-07-26 0.005491 -0.042015 -0.001786 -0.000646 -0.005536 -0.010984 -0.031002 -0.027286 -0.012924 0.000000 -0.004979

1880 rows × 12 columns

3.3.2 - Plotando o gráfico do histórico de taxas de retorno¶

In [16]:
figura = px.line(title = 'Histórico de retorno das ações')
for i in taxas_retorno_date.columns[1:]:
  figura.add_scatter(x=taxas_retorno_date["Data de Negociação"], y=taxas_retorno_date[i], name=i)
figura.show()

3.3.3 - Calculando a covariância entre taxas de retorno¶

Aqui é calculada a covariância entre as taxas de retorno das ações pela função cov() de um DataFrame do Pandas.

Obs.: Considerando-se as linhas e colunas da tabela abaixo, a covariância da linha com o correspondente dela mesmo nas colunas é a variância dela.

In [17]:
taxas_retorno.cov()
Out[17]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
PRIO3 0.002257 0.000403 0.000335 0.000180 0.000257 0.000335 0.000249 0.000308 0.000305 0.000550 0.000356
JHSF3 0.000403 0.001133 0.000304 0.000154 0.000247 0.000380 0.000451 0.000226 0.000349 0.000402 0.000294
SANB1 0.000335 0.000304 0.000555 0.000156 0.000147 0.000294 0.000157 0.000173 0.000195 0.000365 0.000291
VIVT3 0.000180 0.000154 0.000156 0.000360 0.000102 0.000155 0.000070 0.000094 0.000094 0.000138 0.000138
ENEV3 0.000257 0.000247 0.000147 0.000102 0.001183 0.000169 0.000325 0.000177 0.000238 0.000148 0.000158
MULT3 0.000335 0.000380 0.000294 0.000155 0.000169 0.000567 0.000369 0.000186 0.000292 0.000291 0.000271
SIMH3 0.000249 0.000451 0.000157 0.000070 0.000325 0.000369 0.000853 0.000201 0.000478 0.000175 0.000213
UNIP6 0.000308 0.000226 0.000173 0.000094 0.000177 0.000186 0.000201 0.000826 0.000228 0.000273 0.000183
STBP3 0.000305 0.000349 0.000195 0.000094 0.000238 0.000292 0.000478 0.000228 0.001398 0.000210 0.000202
GOAU4 0.000550 0.000402 0.000365 0.000138 0.000148 0.000291 0.000175 0.000273 0.000210 0.001172 0.000364
^BVSP 0.000356 0.000294 0.000291 0.000138 0.000158 0.000271 0.000213 0.000183 0.000202 0.000364 0.000269

3.3.4 - Calculando a correlação entre as taxas de retorno¶

Aqui é calculada a correlação entre taxas de retorno das ações pela função corr() de um DataFrame do Pandas.

In [18]:
taxas_retorno.corr()
Out[18]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP
PRIO3 1.000000 0.251976 0.298939 0.199538 0.157096 0.296470 0.235414 0.225308 0.171714 0.337938 0.454648
JHSF3 0.251976 1.000000 0.383435 0.241030 0.213500 0.474783 0.568460 0.234082 0.277742 0.348704 0.530412
SANB1 0.298939 0.383435 1.000000 0.349420 0.181309 0.523892 0.256232 0.255393 0.221543 0.452910 0.750662
VIVT3 0.199538 0.241030 0.349420 1.000000 0.155882 0.343063 0.174374 0.171452 0.132793 0.211781 0.442459
ENEV3 0.157096 0.213500 0.181309 0.155882 1.000000 0.205972 0.487354 0.179281 0.185448 0.125891 0.278341
MULT3 0.296470 0.474783 0.523892 0.343063 0.205972 1.000000 0.520520 0.271960 0.327813 0.357451 0.690842
SIMH3 0.235414 0.568460 0.256232 0.174374 0.487354 0.520520 1.000000 0.264237 0.555703 0.252325 0.562866
UNIP6 0.225308 0.234082 0.255393 0.171452 0.179281 0.271960 0.264237 1.000000 0.212404 0.277315 0.389331
STBP3 0.171714 0.277742 0.221543 0.132793 0.185448 0.327813 0.555703 0.212404 1.000000 0.164470 0.327717
GOAU4 0.337938 0.348704 0.452910 0.211781 0.125891 0.357451 0.252325 0.277315 0.164470 1.000000 0.646642
^BVSP 0.454648 0.530412 0.750662 0.442459 0.278341 0.690842 0.562866 0.389331 0.327717 0.646642 1.000000

3.3.5 - Plotando o gráfico de correlação entre as taxas de retorno¶

Por fim, o gráfico de calor ilustra a correlação entre as taxas de retorno das ações contidas na carteira. Quanto mais próximo de 1, maior é o impacto de uma ação em relação a outra. O aumento de uma ação tem o impacto proporcional no preço de outra ação, conforme o peso ilustrado no mapa de calor.

Por exemplo: o aumento de 1% em PRIO3 tem o impacto de aumento de 0,45% no ^BVSP.

In [19]:
plt.figure(figsize=(8,8))
sns.heatmap(taxas_retorno.corr(), annot=True);

4 - Montando uma Carteira de Ativos¶


4.1 - Incrementando o dataset¶


Aqui será adicionada ao dataset criado na seção anterior a média das taxas de retorno que compõem a carteira. A idéia é preparar os dados para comparação do desempenho da carteira com a índice IBOVESPA (^BVSP).

Obs.: A média dos valores das colunas não é calculada pela função mean(), dado que as colunas podem possuir valores nulos.

In [20]:
carteira = taxas_retorno_date.iloc[:,1:taxas_retorno_date.shape[1] - 1] # Calcula as somas de valores de todas as colunas, com exceção da coluna "Data de Negociação" e "^BVSP"
numero_de_acoes = carteira.shape[1]
taxas_retorno_date["CARTEIRA"] = carteira.sum(axis="columns") / numero_de_acoes
taxas_retorno_date
Out[20]:
Data de Negociação PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 ^BVSP CARTEIRA
0 2015-01-02 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 0.000000
1 2015-01-05 -0.077962 -0.036534 -0.007101 -0.020030 -0.074108 -0.001357 NaN -0.043485 0.000000 -0.048282 -0.020724 -0.030886
2 2015-01-06 -0.103451 0.004640 0.007101 -0.025131 0.025318 0.048575 NaN 0.021979 0.000000 0.093457 0.010134 0.007249
3 2015-01-07 -0.002729 0.000000 0.040822 0.033726 0.024693 0.033902 NaN 0.000000 0.000000 0.070758 0.030003 0.020117
4 2015-01-08 0.032261 0.088553 -0.016743 0.039848 0.024098 -0.010682 NaN 0.000000 0.000000 -0.034327 0.009657 0.012301
... ... ... ... ... ... ... ... ... ... ... ... ... ...
1875 2022-07-20 -0.004482 0.012121 -0.007057 -0.013136 0.018475 -0.001254 0.038590 0.007000 0.022582 0.002012 0.000427 0.007485
1876 2022-07-21 0.025722 -0.022629 0.002122 0.001918 -0.015028 -0.006717 0.003150 -0.004624 -0.008006 -0.003020 0.007561 -0.002711
1877 2022-07-22 -0.007471 -0.014185 -0.027585 -0.003198 -0.012465 0.010059 0.001048 0.000339 0.004812 0.002014 -0.001091 -0.004663
1878 2022-07-25 0.040628 -0.001787 0.017642 -0.008794 0.009709 -0.007535 -0.005249 0.003271 -0.003205 0.007018 0.013505 0.005170
1879 2022-07-26 0.005491 -0.042015 -0.001786 -0.000646 -0.005536 -0.010984 -0.031002 -0.027286 -0.012924 0.000000 -0.004979 -0.012669

1880 rows × 13 columns

4.2 - Visualizando a comparação entre a Carteira e o IBOVESPA¶

4.2.1 Exibindo a tabela de valores comparados por data¶

Primeiro é exibido um DataFrame do Pandas filtrado com as colunas que serão plotados no gráfico para facilitar a visualização dos números.

In [21]:
taxas_retorno_port = taxas_retorno_date.filter(["Data de Negociação", "CARTEIRA", "^BVSP"])
taxas_retorno_port
Out[21]:
Data de Negociação CARTEIRA ^BVSP
0 2015-01-02 0.000000 NaN
1 2015-01-05 -0.030886 -0.020724
2 2015-01-06 0.007249 0.010134
3 2015-01-07 0.020117 0.030003
4 2015-01-08 0.012301 0.009657
... ... ... ...
1875 2022-07-20 0.007485 0.000427
1876 2022-07-21 -0.002711 0.007561
1877 2022-07-22 -0.004663 -0.001091
1878 2022-07-25 0.005170 0.013505
1879 2022-07-26 -0.012669 -0.004979

1880 rows × 3 columns

4.2.2 Plotando o gráfico de linhas com a comparação¶

Em seguida, o gráfico é plotado contendo a comparação entre o desempenho da carteira e do IBOVESPA no mesmo período. A linha verde representa a média da taxa de retorno da carteira. Pode-se observar que os desempenhos são bastante parecidos, tendo a carteira um perfil com maior variação em função do número de títulos que a compõem ser menor do que o do IBOVESPA.

In [22]:
figura = px.line(title = 'Comparação de retorno Carteira x Ibovespa')
for i in taxas_retorno_port.columns[1:]:
  figura.add_scatter(x=taxas_retorno_port["Data de Negociação"], y = taxas_retorno_port[i], name=i)
figura.add_hline(y=taxas_retorno_port['CARTEIRA'].mean(), line_color="green", line_dash="dot", )
figura.show()

4.2.3 - Calculando a correlação entre as taxas de retorno¶

Mais uma vez é calculada a correlação entre taxas de retorno, dessa vez entre a carteira e o IBOVESPA, como descrito na tabela abaixo.

In [23]:
taxas_retorno_port_corr = taxas_retorno_date.filter(["CARTEIRA", "^BVSP"])
taxas_retorno_port_corr
Out[23]:
CARTEIRA ^BVSP
0 0.000000 NaN
1 -0.030886 -0.020724
2 0.007249 0.010134
3 0.020117 0.030003
4 0.012301 0.009657
... ... ...
1875 0.007485 0.000427
1876 -0.002711 0.007561
1877 -0.004663 -0.001091
1878 0.005170 0.013505
1879 -0.012669 -0.004979

1880 rows × 2 columns

4.2.4 - Plotando o gráfico de correlação entre as taxas de retorno¶

Como na seção anterior, aqui é exibido o gráfico de calor que ilustra a correlação entre as taxas de retorno das ações contidas na carteira. Quanto mais próximo de 1, maior é o impacto de uma ação em relação a outra. O aumento de uma ação tem o impacto proporcional no preço de outra ação, conforme o peso ilustrado no mapa de calor. Se a Carteira aumenta 1%, o IBOVESPA aumenta 0,83% e vice-versa.

In [24]:
plt.figure(figsize=(8,8))
sns.heatmap(taxas_retorno_port_corr.corr(), annot=True);

4.3 - Alocação Aleatória de Ativos - Portfólio Markowitz¶


Na seção 4.1, foi utilizada a média aritmética para obter a taxa de desempenho. Aqui serão criadas carteiras aleatórias, com pesos diferentes para verificar as melhores alocações de recursos.

4.3.1 - Preparando o novo dataset¶

Criação de um novo dataset, retirando o IBOVESPA, dado que a comparação será feita entre as carteiras aletórias.

In [25]:
acoes_port = acoes_df.copy()
acoes_port.drop(labels = ['^BVSP'], axis=1, inplace=True)
acoes_port
Out[25]:
Data de Negociação PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4
0 2015-01-02 0.440000 2.23 12.720000 37.820000 5.989072 14.753333 NaN 3.357142 0.95 10.82
1 2015-01-05 0.407000 2.15 12.630000 37.070000 5.561281 14.733333 NaN 3.214285 0.95 10.31
2 2015-01-06 0.367000 2.16 12.720000 36.150002 5.703878 15.466666 NaN 3.285713 0.95 11.32
3 2015-01-07 0.366000 2.16 13.250000 37.389999 5.846475 16.000000 NaN 3.285713 0.95 12.15
4 2015-01-08 0.378000 2.36 13.030000 38.910000 5.989072 15.830000 NaN 3.285713 0.95 11.74
... ... ... ... ... ... ... ... ... ... ... ...
1875 2022-07-20 22.260000 5.81 28.240000 46.889999 14.750000 23.900000 9.51 88.879997 6.27 9.95
1876 2022-07-21 22.840000 5.68 28.299999 46.980000 14.530000 23.740000 9.54 88.470001 6.22 9.92
1877 2022-07-22 22.670000 5.60 27.530001 46.830002 14.350000 23.980000 9.55 88.500000 6.25 9.94
1878 2022-07-25 23.610001 5.59 28.020000 46.419998 14.490000 23.799999 9.50 88.790001 6.23 10.01
1879 2022-07-26 23.740000 5.36 27.969999 46.389999 14.410000 23.540001 9.21 86.400002 6.15 10.01

1880 rows × 11 columns

4.3.2 - Simulando uma alocação aleatória de recursos financeiros¶

Primeiramente é criada uma função para, a partir de um dataset e valor de investimento, calcular uma distribuição de pesos aleatória e taxas de retorno a partir da alocações de recursos feitas nas ações da carteira definida.

A função retorna:

  1. dataset contendo os valores das ações atualizados diariamente, de acordo com o valor alocado;
  2. datas de referência;
  3. pesos aleatórios utilizados;
  4. valor total atualizado da carteira, considerando o valor investido
In [26]:
def alocacao_ativos(dataset, dinheiro_total, seed = 0, melhores_pesos = []):
  dataset = dataset.copy()

  if seed != 0:
    np.random.seed(seed)

  if len(melhores_pesos) > 0:
    pesos = melhores_pesos
  else:  
    pesos = np.random.random(len(dataset.columns) - 1)
    pesos = pesos / pesos.sum()

  colunas = dataset.columns[1:]

  for i in colunas:
    dataset[i] = (dataset[i] / dataset[i][0])

  for i, acao in enumerate(dataset.columns[1:]):
    dataset[acao] = dataset[acao] * pesos[i] * dinheiro_total
  
  dataset['soma valor'] = dataset.iloc[:,1:dataset.shape[1]].sum(axis = 1)

  datas = dataset["Data de Negociação"]

  dataset.drop(labels = ["Data de Negociação"], axis = 1, inplace = True)
  dataset['taxa retorno'] = 0.0

  for i in range(1, len(dataset)):
    dataset['taxa retorno'][i] = np.log(dataset['soma valor'][i] / dataset['soma valor'][i - 1]) * 100

  acoes_pesos = pd.DataFrame(data = {'Ações': colunas, 'Pesos': pesos})

  return dataset, datas, acoes_pesos, dataset.loc[len(dataset) - 1]['soma valor']

Aqui é chamada a função de simulação, passando como parâmetros o dataset criado, valor de R$10.000,00 e o seed = 10 a ser utilizado na função de randomização.

In [27]:
dataset, datas, acoes_pesos, soma_valor = alocacao_ativos(acoes_port, 10000, 10)
In [28]:
dataset
Out[28]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4 soma valor taxa retorno
0 1874.925937 50.443831 1540.271898 1820.192204 1211.770663 546.435604 NaN 1848.697777 411.074560 214.736388 9518.548861 0.000000
1 1734.306524 48.634188 1529.373729 1784.096377 1125.215657 545.694824 NaN 1770.029803 411.074560 204.614816 9153.040479 -3.915629
2 1563.858746 48.860394 1540.271898 1739.818918 1154.067294 572.856116 NaN 1809.363527 411.074560 224.659512 9064.830963 -0.968392
3 1559.597479 48.860394 1604.449860 1799.497248 1182.919027 592.609792 NaN 1809.363527 411.074560 241.131895 9249.503781 2.016771
4 1610.731799 53.384500 1577.809905 1872.651479 1211.770663 586.313310 NaN 1809.363527 411.074560 232.994937 9366.094681 1.252631
... ... ... ... ... ... ... ... ... ... ... ... ...
1875 94854.209140 131.425405 3419.597258 2256.711053 2984.371826 885.210863 NaN 48944.088372 2713.092119 197.470155 156386.176191 -0.014504
1876 97325.702110 128.484730 3426.862627 2261.042559 2939.859107 879.284771 NaN 48718.313364 2691.456533 196.874773 158567.880574 1.385433
1877 96601.298578 126.675088 3333.623075 2253.823504 2903.439786 888.173909 NaN 48734.832974 2704.437926 197.271688 157743.576527 -0.521199
1878 100606.824468 126.448888 3392.957419 2234.090942 2931.765903 881.507038 NaN 48894.529541 2695.783733 198.660938 161962.568872 2.639447
1879 101160.776286 121.246161 3386.902753 2232.647168 2915.579495 871.877191 NaN 47578.414049 2661.166961 198.660938 161127.271001 -0.517070

1880 rows × 12 columns

In [29]:
acoes_pesos
Out[29]:
Ações Pesos
0 PRIO3 0.187493
1 JHSF3 0.005044
2 SANB1 0.154027
3 VIVT3 0.182019
4 ENEV3 0.121177
5 MULT3 0.054644
6 SIMH3 0.048145
7 UNIP6 0.184870
8 STBP3 0.041107
9 GOAU4 0.021474
In [30]:
datas
Out[30]:
0      2015-01-02
1      2015-01-05
2      2015-01-06
3      2015-01-07
4      2015-01-08
          ...    
1875   2022-07-20
1876   2022-07-21
1877   2022-07-22
1878   2022-07-25
1879   2022-07-26
Name: Data de Negociação, Length: 1880, dtype: datetime64[ns]
In [31]:
soma_valor
Out[31]:
161127.2710014405

4.3.3 - Plotando o retorno diário do portfólio¶

Com base na ponderação aleatória e nos recursos investidos em cada ação.

In [32]:
figura = px.line(x = datas, y = dataset['taxa retorno'], title = 'Retorno diário do portfólio',
                labels=dict(x="Data", y="Retorno %"))
figura.add_hline(y = dataset['taxa retorno'].mean(), line_color="red", line_dash="dot", )
figura.show()

4.3.4 - Plotando a evolução diária do patrimônio (por ação)¶

Com base na ponderação aleatória e nos recursos investidos em cada ação.

In [33]:
figura = px.line(title = 'Evolução do patrimônio')
for i in dataset.drop(columns = ['soma valor', 'taxa retorno']).columns:
  figura.add_scatter(x = datas, y = dataset[i], name = i)
figura.show()

4.3.5 - Plotando a evolução diária do patrimônio (geral)¶

Com base na ponderação aleatória e nos recursos investidos em cada ação. A linha verde representa a média da carteira.

In [34]:
figura = px.line(x = datas, y = dataset['soma valor'], 
                 title = 'Evolução do patrimônio da Carteira',
                 labels=dict(x="Data", y="Valor R$"))
figura.add_hline(y = dataset['soma valor'].mean(), 
                 line_color="green", line_dash="dot", )
figura.show()

4.3.6 - Mais estatísticas sobre o portfólio aleatório¶

Aqui, algumas estatísticas adicionais sobre o portfólio aleatório.

In [35]:
# Retorno
dataset.loc[len(dataset) - 1]['soma valor'] / dataset.loc[0]['soma valor'] - 1
Out[35]:
15.927713809889948
In [36]:
# Desvio-Padrão
dataset['taxa retorno'].std()
Out[36]:
2.4419700993463254

O índice de Sharpe dá uma ideia de retorno em relação ao risco. Quanto maior o índice, mais convervadora a carteira e menor o retorno.

In [37]:
# Sharpe Ratio
(dataset['taxa retorno'].mean() / dataset['taxa retorno'].std())
Out[37]:
0.061620811077965895

Valor ganho, excluindo-se o valor investido na data inicial.

In [38]:
dinheiro_total = 10000
soma_valor - dinheiro_total
Out[38]:
151127.2710014405

5 - Simulação da Fronteira Eficiente¶


Por fim, utilizando-se dos conhecimentos e estruturas criadas nas seções anteriores, será feita uma simulação com 1000 carteiras aleatórias e obtido o portfólio que dá o melhor índice de Sharpe.

In [39]:
acoes_port
Out[39]:
Data de Negociação PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4
0 2015-01-02 0.440000 2.23 12.720000 37.820000 5.989072 14.753333 NaN 3.357142 0.95 10.82
1 2015-01-05 0.407000 2.15 12.630000 37.070000 5.561281 14.733333 NaN 3.214285 0.95 10.31
2 2015-01-06 0.367000 2.16 12.720000 36.150002 5.703878 15.466666 NaN 3.285713 0.95 11.32
3 2015-01-07 0.366000 2.16 13.250000 37.389999 5.846475 16.000000 NaN 3.285713 0.95 12.15
4 2015-01-08 0.378000 2.36 13.030000 38.910000 5.989072 15.830000 NaN 3.285713 0.95 11.74
... ... ... ... ... ... ... ... ... ... ... ...
1875 2022-07-20 22.260000 5.81 28.240000 46.889999 14.750000 23.900000 9.51 88.879997 6.27 9.95
1876 2022-07-21 22.840000 5.68 28.299999 46.980000 14.530000 23.740000 9.54 88.470001 6.22 9.92
1877 2022-07-22 22.670000 5.60 27.530001 46.830002 14.350000 23.980000 9.55 88.500000 6.25 9.94
1878 2022-07-25 23.610001 5.59 28.020000 46.419998 14.490000 23.799999 9.50 88.790001 6.23 10.01
1879 2022-07-26 23.740000 5.36 27.969999 46.389999 14.410000 23.540001 9.21 86.400002 6.15 10.01

1880 rows × 11 columns

In [40]:
log_ret = acoes_port.copy()
log_ret.drop(labels = ["Data de Negociação"], axis = 1, inplace = True)
log_ret = np.log(log_ret/log_ret.shift(1))
log_ret
Out[40]:
PRIO3 JHSF3 SANB1 VIVT3 ENEV3 MULT3 SIMH3 UNIP6 STBP3 GOAU4
0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 -0.077962 -0.036534 -0.007101 -0.020030 -0.074108 -0.001357 NaN -0.043485 0.000000 -0.048282
2 -0.103451 0.004640 0.007101 -0.025131 0.025318 0.048575 NaN 0.021979 0.000000 0.093457
3 -0.002729 0.000000 0.040822 0.033726 0.024693 0.033902 NaN 0.000000 0.000000 0.070758
4 0.032261 0.088553 -0.016743 0.039848 0.024098 -0.010682 NaN 0.000000 0.000000 -0.034327
... ... ... ... ... ... ... ... ... ... ...
1875 -0.004482 0.012121 -0.007057 -0.013136 0.018475 -0.001254 0.038590 0.007000 0.022582 0.002012
1876 0.025722 -0.022629 0.002122 0.001918 -0.015028 -0.006717 0.003150 -0.004624 -0.008006 -0.003020
1877 -0.007471 -0.014185 -0.027585 -0.003198 -0.012465 0.010059 0.001048 0.000339 0.004812 0.002014
1878 0.040628 -0.001787 0.017642 -0.008794 0.009709 -0.007535 -0.005249 0.003271 -0.003205 0.007018
1879 0.005491 -0.042015 -0.001786 -0.000646 -0.005536 -0.010984 -0.031002 -0.027286 -0.012924 0.000000

1880 rows × 10 columns

In [41]:
np.random.seed(42)
num_ports = 1000
all_weights = np.zeros((num_ports, len(acoes_port.columns[1:])))
ret_arr = np.zeros(num_ports)
vol_arr = np.zeros(num_ports)
sharpe_arr = np.zeros(num_ports)

for x in range(num_ports):
    # Weights
    weights = np.array(np.random.random(log_ret.shape[1]))
    weights = weights/np.sum(weights)
    
    # Save weights
    all_weights[x,:] = weights
    
    # Expected return
    ret_arr[x] = np.sum((log_ret.mean() * weights))
    
    # Expected volatility
    vol_arr[x] = np.sqrt(np.dot(weights.T, np.dot(log_ret.cov(), weights)))
    
    # Sharpe Ratio
    sharpe_arr[x] = ret_arr[x]/vol_arr[x]
In [42]:
print("Max Sharpe Ratio: {}". format(sharpe_arr.max()))
print("Local do Max Sharpe Ratio: {}".format(sharpe_arr.argmax()))
Max Sharpe Ratio: 0.05765141684553508
Local do Max Sharpe Ratio: 842
In [43]:
# Pesos do Portfólio do Max Sharpe Ratio
print(all_weights[sharpe_arr.argmax(),:])
[0.18770876 0.08326079 0.07486367 0.00378353 0.0427857  0.10152048
 0.03836667 0.32361831 0.10131189 0.04278019]
In [44]:
# salvando os dados do Max Sharpe Ratio
max_sr_ret = ret_arr[sharpe_arr.argmax()]
max_sr_vol = vol_arr[sharpe_arr.argmax()]
print(max_sr_ret)
print(max_sr_vol)
0.0011915637549334255
0.020668421005609828
In [45]:
plt.figure(figsize=(12,8))
plt.scatter(vol_arr, ret_arr, c=sharpe_arr, cmap='viridis')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatilidade')
plt.ylabel('Retorno')
plt.scatter(max_sr_vol, max_sr_ret,c='black', s=200) # black dot
plt.show()

Nós podemos ver no gráfico acima o conjunto de portfólios simulados, pois o peso $w_i$ de cada ativo foi simulado e criamos um conjunto de $n = 1000$ carteiras e escolhemos no ponto vermelho a que tem maior Sharpe Ratio, que é a razão retorno sobre a volatilidade. Esse dado nos da uma noção do portfólio ponderado pelo risco.

In [46]:
def get_ret_vol_sr(weights):
    weights = np.array(weights)
    ret = np.sum(log_ret.mean() * weights)
    vol = np.sqrt(np.dot(weights.T, np.dot(log_ret.cov(), weights)))
    sr = ret/vol
    return np.array([ret, vol, sr])

def neg_sharpe(weights):
# the number 2 is the sharpe ratio index from the get_ret_vol_sr
    return get_ret_vol_sr(weights)[2] * -1

def check_sum(weights):
    #return 0 if sum of the weights is 1
    return np.sum(weights)-1
In [47]:
cons = ({'type': 'eq', 'fun': check_sum})
bounds = tuple((0, 1) for i in range(10))
init_guess = tuple(0.2 for i in range(10))
In [48]:
op_results = optimize.minimize(neg_sharpe, init_guess, method="SLSQP", bounds= bounds, constraints=cons)
print(op_results)
     fun: -0.06883155328212162
     jac: array([-2.32809223e-04,  1.17236711e-02,  6.38942234e-03,  7.94762000e-03,
        3.25346459e-03,  1.51013732e-02,  6.32125419e-03,  6.62282109e-05,
        1.28329732e-04,  3.72350272e-02])
 message: 'Optimization terminated successfully'
    nfev: 99
     nit: 9
    njev: 9
  status: 0
 success: True
       x: array([2.42976077e-01, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00,
       0.00000000e+00, 1.30104261e-18, 0.00000000e+00, 6.53474890e-01,
       1.03549033e-01, 0.00000000e+00])
In [49]:
# frontier_y = np.linspace(-0.0006, 0.0008, 200)
frontier_y = np.linspace(0.0002, 0.00135, 200)
In [50]:
def minimize_volatility(weights):
    return get_ret_vol_sr(weights)[1]
In [51]:
frontier_x = []

for possible_return in frontier_y:
    cons = ({'type':'eq', 'fun':check_sum},
            {'type':'eq', 'fun': lambda w: get_ret_vol_sr(w)[0] - possible_return})
    
    result = optimize.minimize(minimize_volatility,init_guess,method='SLSQP', bounds=bounds, constraints=cons)
    frontier_x.append(result['fun'])
In [52]:
plt.figure(figsize=(12,8))
plt.scatter(vol_arr, ret_arr, c=sharpe_arr, cmap='viridis')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatilidade')
plt.ylabel('Retorno')
plt.plot(frontier_x,frontier_y, 'r--', linewidth=3)
plt.scatter(max_sr_vol, max_sr_ret,c='black', s=200)
# plt.savefig('cover.png')
plt.show()